MotionNumber v0.1.7
Transition, format, and localize numbers with a ~3kB Framer Motion component. Accessible and customizable.
// Basic usage
import MotionNumber from 'motion-number'
<MotionNumber
value={value}
format={{ notation: 'compact' }} // Intl.NumberFormat() options
locales="en-US" // Intl.NumberFormat() locales
/>
See MDN’s Intl.NumberFormat()
reference for a full list of locales
and format
options.
Customizing
MotionNumber is a Motion component, so it accepts a transition
prop to customize
the transitions:
import { easeOut } from 'framer-motion'
<MotionNumber
transition={{
// Applied to layout animations on individual characters:
layout: { type: 'spring', duration: 0.7, bounce: 0 },
// Used for the digit animations:
y: { type: 'spring', duration: 0.7, bounce: 0.25 },
// Opacity applies to entering/exiting characters.
// Note the use of the times array, explained below:
opacity: { duration: 0.7, ease: easeOut, times: [0, 0.3] } // 0.3s perceptual duration
}}
/>
One important note: if you override opacity
, make sure to give it the same duration as the layout animation.
This improves animation performance by ensuring Framer Motion doesn’t remove exiting children
until the layout animation ends. If you want the opacity transitions to look shorter than the layout animations,
you can use the times
array with the format [0, perceptualDuration]
,
as shown above. Also, make sure to use an easing function rather than strings like 'linear'
or 'easeIn'
,
as they seem to work better. If you’re using <MotionConfig>
to set a transition
, make sure to check the
opacity settings there as well.
Beyond the transitions, there’s some CSS properties you can use to customize the display:
--mask-height
/--mask-width
These adjust the height and width of the gradient fade-out masks at the edges of the number.
--mask-height
also gets used as the vertical padding for the number.
line-height
In my opinion, the transitions look better with small vertical gaps between numbers, because it reduces the distance
they travel.
I recommend using the smallest line-height
that fits all your component’s characters:
<MotionNumber style={{ lineHeight: 0.8 }} /* ... */>
The exact value will depend on your font and formatting options. MotionNumber uses a safe default line-height
of 1,
set through an inline style for simplicity. This means to override it globally you’ll have to use !important
(sorry about that; React 19 should clean this up):
[data-motion-number] {
line-height: 0.8 !important;
}
Grouping
MotionNumber has four render props that can be used to synchronize other content with the number transitions:
before
adds elements before MotionNumberfirst
prepends elements to MotionNumberlast
appends elements to MotionNumberafter
adds elements after MotionNumber
These can be combined to create interesting effects:
<MotionNumber
value={value}
format={{ style: 'currency', currency: 'USD' }}
after={() => (
<MotionNumber
value={diff}
format={{ style: 'percent', maximumFractionDigits: 2 }}
animate={{ backgroundColor: diff > 0 ? '#34d399' : '#ef4444' }}
style={{ borderRadius: 999 }}
first={() => (
<Arrow />
)}
/>
)}
/>
These are also helpful because LayoutGroup doesn’t currently work with MotionNumber; see the note in limitations.
LazyMotion
If you’re using <LazyMotion>
, you can import the lazy version of
MotionNumber with:
import MotionNumber from 'motion-number/lazy'
The API is the same.
Limitations
- Scientific and engineering notations aren’t currently supported
- Selected text inside the number will not respect color changes from
::selection
, if any <LayoutGroup>
doesn’t currently work with MotionNumber and causes noticeable jitters in the number animations. This is likely due to MotionNumber’s unusual two-step rendering process, where new digits are added before the layout animations occur.
Credits
Built by Max Barvian. Heavily inspired by the Family iOS app.
Technique for the mask-image
from Artur Bień.